﻿using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Xml;
using System.Xml.Serialization;

namespace PageCache
{
    public class CacheService
    {
        #region 私有字段

        Setting.Setting setting;

        Store.ILastDataList lastReadDataList = null;

        Store.IStoreDataList storeDataList = null;

        Common.HttpClient httpClient;

        Common.Log errorLog = null;

        public Common.Log ErrorLog
        {
            get { return errorLog; }
        }

        Config.Config config = null;

        #endregion

        /// <summary>
        /// 初始化
        /// </summary>
        /// <param name="setting"></param>
        public CacheService(Setting.Setting setting)
        {
            config = setting.Config;

            this.setting = setting;

            //this.module = module;

            this.httpClient = new Common.HttpClient();

            this.httpClient.ReceiveTimeout = config.NetReceiveTimeout;

            this.httpClient.SendTimeout = config.NetSendTimeout;

            if (!string.IsNullOrEmpty(config.ErrorLogPath))
            {
                this.errorLog = new Common.Log(config.ErrorLogPath);

                this.httpClient.ErrorLog = this.errorLog;
            }

            this.lastReadDataList = Store.Factory.CreateLastDataListInstance(setting, config.ListStoreType, config.ListStoreConnectionString);

            this.storeDataList = Store.Factory.CreateStoreDataListInstance(setting, config.ListStoreType, config.ListStoreConnectionString);
        }

        const string SYSTEM_ERROR_MESSAGE = "The system is busy now. Please try later.";

        public void Process(HttpContext context)
        {
            RequestInfo info = null;

            Process(context, out info);
        }


        //?=404;

        /// <summary>
        /// 入口
        /// </summary>
        /// <param name="context"></param>
        /// <param name="info"></param>
        public void Process(HttpContext context, out RequestInfo info)
        {
            info = null;

            //忽略 url http://test.yxdown.com/404.html?404;http://test.yxdown.com:80/test.ashx
            var reg_url_ignore = new Regex(@"\://.*(4|5|6)\d+;(http|https)\://");

            if (reg_url_ignore.IsMatch(context.Request.Url.ToString()))
            {
                return;
            }

            if (!string.IsNullOrEmpty(setting.Config.SkipUAMatch) && !string.IsNullOrEmpty(context.Request.UserAgent))
            {
                var skipUA = new System.Text.RegularExpressions.Regex(setting.Config.SkipUAMatch, RegexOptions.IgnoreCase);

                if (skipUA.IsMatch(context.Request.UserAgent))
                {
                    return;
                }
            }


            var rule = setting.Rules.Get(context);

            if (rule != null)
            {
                context.Response.Headers.Set("PageCache", "2016/11/02, Lib360@qq.com");

                /*
                if (EchoClientCache(context, rule.ConfigRule.CacheSeconds))
                {
                    context.ApplicationInstance.CompleteRequest();
                    return;
                }
                */

                info = RequestInfo.Create(context, rule);

                if (info != null)
                {


                    if (!EchoData(info))
                    {
                        context.Response.StatusCode = 503;

                        if (errorLog != null)
                        {
                            errorLog.Write(SYSTEM_ERROR_MESSAGE + info.ToString());
                        }
                    }
                    else
                    {
                        context.Response.Headers.Set("PageCacheInfo", "KEY:" + info.Key + ",TYPE:" + info.Type);
                    }


                    context.ApplicationInstance.CompleteRequest();
                }
            }
        }

        string GetClientIP(HttpContext context)
        {
            string ip = null;

            if (!string.IsNullOrEmpty(this.setting.Config.CDNIPHead))
            {
                ip = context.Request.Headers[this.setting.Config.CDNIPHead];
            }


            if (string.IsNullOrEmpty(ip))
            {
                if (context.Request.ServerVariables["HTTP_VIA"] != null)
                {
                    ip = context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];

                    if (ip == null)
                    {
                        ip = context.Request.ServerVariables["REMOTE_ADDR"];
                    }
                }
                else
                {
                    ip = context.Request.ServerVariables["REMOTE_ADDR"];
                }

                if (string.Compare(ip, "unknown", true) == 0)
                {
                    return context.Request.UserHostAddress;
                }
            }
            return ip;
        }

        const string allip = "*";

        bool HasValidRefreshKey(HttpContext context)
        {

            string rk = setting.Config.RefreshKey;
            string rawurl = context.Request.RawUrl;

            string rip = config.RefreshIP ?? string.Empty;

            bool isvalidip = string.IsNullOrEmpty(rip)
                || rip.Equals(allip)
                || rip.Contains(GetClientIP(context));

            if (isvalidip)
            {
                if (rawurl.IndexOf(rk) >= 0)
                {
                    return true;
                }

                if (context.Request.Headers.AllKeys.Contains(rk))
                {
                    return true;
                }
            }

            return false;
        }

        bool RedirectOrPermanent(RequestInfo info)
        {
            var context = info.Context;

            var rule = info.Rule;

            if (rule != null && (rule.ConfigRule.Redirect || rule.ConfigRule.Permanent))
            {
                string rawurl = context.Request.RawUrl;

                if (!rule.MatchRegex.IsMatch(rawurl))
                {
                    if (rule.ConfigRule.Permanent)
                    {
                        context.Response.RedirectPermanent(context.Request.Path, false);
                    }
                    else
                    {
                        context.Response.Redirect(context.Request.Path, false);
                    }

                    return true;
                }
            }

            return false;
        }

        bool EchoData(RequestInfo info)
        {

            var now = DateTime.Now;
            int cacheSeconds = info.Rule.ConfigRule.CacheSeconds;

            Store.StoreData olddata = null;

            bool refresh = HasValidRefreshKey(info.Context);

            if (refresh)
            {
                lastReadDataList.Delete(info.Type, info.Key);

                storeDataList.Delete(info.Type, info.Key);

                if (info.Store != null)
                {
                    info.Store.Delete(info.Type, info.Key);
                }
            }

            if (!refresh)
            {
                if (RedirectOrPermanent(info))
                {
                    return true;
                }
            }


            //没有强制刷新的时候 从缓存读取
            #region 从缓存读取
            if (!refresh)
            {

                //尝试从最后的数据列中读取缓存
                if (true)
                {
                    Store.StoreData data = lastReadDataList.Get(info.Type, info.Key);

                    if (data != null)
                    {
                        olddata = data;

                        if (setting.Config.ReadOnly)
                        {
                            if (EchoData(info, data))
                            {
                                return true;
                            }
                        }



                        if (data.CreatedDate.AddSeconds(cacheSeconds) >= now)
                        {
                            if (EchoData(info, data))
                            {
                                return true;
                            }
                        }
                    }
                }

                //尝试从存储队列读取缓存
                if (true)
                {
                    Store.StoreData data = storeDataList.Get(info.Type, info.Key);
                    if (data != null)
                    {
                        olddata = data;

                        if (setting.Config.ReadOnly)
                        {
                            if (EchoData(info, data))
                            {
                                return true;
                            }
                        }

                        if (data.CreatedDate.AddSeconds(cacheSeconds) >= now)
                        {
                            if (EchoData(info, data))
                            {
                                return true;
                            }
                        }
                    }
                }



                //从Store读取缓存
                if (info.Store != null)
                {
                    Store.StoreData data = info.Store.GetData(info.Type, info.Key);

                    if (data != null)
                    {
                        olddata = data;

                        if (setting.Config.ReadOnly)
                        {
                            if (EchoData(info, data))
                            {
                                return true;
                            }
                        }

                        if (data.CreatedDate.AddSeconds(cacheSeconds) >= now)
                        {
                            lastReadDataList.Add(data);

                            if (EchoData(info, data))
                            {
                                return true;
                            }
                        }
                    }
                }
            }

            #endregion

            if (refresh)
            {
                return TryCreateAndEchoData(info, olddata);
            }
            else
            {
                //创建优先
                if (info.Rule.CreateFirst)
                {
                    return TryCreateAndEchoData(info, olddata);
                }
                else
                {
                    return TryEchoAndCreateData(info, olddata);
                }
            }
        }

        bool TryCreateAndEchoData(RequestInfo info, Store.StoreData olddata)
        {
            Store.StoreData outdata = null;

            if (TryCreateAndSaveData(info, out outdata))
            {

                if (EchoData(info, outdata))
                {
                    return true;
                }
            }

            //上述步骤执行失败，输出老缓存
            if (olddata != null)
            {
                if (olddata.BodyData != null)
                {
                    if (EchoData(info, olddata))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        bool TryEchoAndCreateData(RequestInfo info, Store.StoreData olddata)
        {

            //优先输出老缓存，再创建保存缓存
            if (olddata != null)
            {
                if (EchoData(info, olddata))
                {
                    Store.StoreData outdata;

                    TryCreateAndSaveData(info, out outdata);

                    return true;
                }
            }
            else
            {
                Store.StoreData outdata = null;

                if (TryCreateAndSaveData(info, out outdata))
                {
                    if (EchoData(info, outdata))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        bool TryCreateAndSaveData(RequestInfo info, out Store.StoreData outdata)
        {
            outdata = null;

            var cdata = CreateData(info);

            if (cdata != null)
            {
                if (cdata.HeadersData != null)
                {
                    //存入store
                    if (cdata.Seconds > 0 && cdata.HeadersData.Length >= 0)
                    {
                        //info.Store.Save(cdata);
                        storeDataList.Add(info.Store.Name, cdata);
                    }
                    //存入 lastRead
                    lastReadDataList.Add(cdata);
                    outdata = cdata;
                    return true;
                }
            }

            return false;
        }

        Store.StoreData CreateData(RequestInfo info)
        {
            Store.StoreData storeData = null;

            string headers = GetRequestHeadersData(info);

            Common.HttpData http_data = httpClient.GetData(info.HostAddress, headers);

            if (http_data != null)
            {
                var store_data = ConvertHttpDataToStoreData(info, http_data);
                if (store_data != null)
                {
                    if (store_data.HeadersData != null)
                    {
                        storeData = store_data;
                    }
                }
            }
            return storeData;
        }

        const int CHUNKED_SIZE = 1024 * 1024 * 2;

        const string Sign_Chunked = "chunked";



        bool EchoData(RequestInfo info, Store.StoreData data)
        {

            if (data == null)
            {
                return false;
            }

            HttpContext context = info.Context;

            var response = context.Response;



            bool echoGZip = IsClientProvidedGZip(context);

            string headerstrings = Encoding.ASCII.GetString(data.HeadersData);

            Common.HttpDataInfo hhs = Common.HttpClient.ParseHeaderString(headerstrings);

            if (hhs != null)
            {
                if (IsErrorHttpStatusCode(hhs.StatusCode))
                {
                    response.StatusCode = hhs.StatusCode;
                    return true;
                }

                response.StatusCode = hhs.StatusCode;

                response.StatusDescription = hhs.StatusDescription;

                foreach (string k in hhs.Headers.Keys)
                {
                    string v = hhs.Headers[k];

                    response.AddHeader(k, v);

                    if (!echoGZip)
                    {
                        if (k == "Content-Type")
                        {
                            EchoContentEncodingCharset(context, v);
                        }
                    }
                }


                if (!IsErrorHttpStatusCode(hhs.StatusCode) && info.Rule.Setting.Config.CacheControlEnable)
                {
                    EchoBrowserCache(info, data);
                }
            }


            var bodyData = data.BodyData;
            if (bodyData != null)
            {

                int totalCount = bodyData.Length;

                if (totalCount > CHUNKED_SIZE)
                {
                    response.BufferOutput = false;

                    var outputStream = response.OutputStream;

                    try
                    {
                        int nowCount = 0;

                        while (nowCount < totalCount)
                        {
                            int leftCount = totalCount - nowCount;

                            if (leftCount < CHUNKED_SIZE)
                            {
                                outputStream.Write(bodyData, nowCount, leftCount);
                                outputStream.Flush();
                                nowCount += leftCount;
                            }
                            else
                            {
                                outputStream.Write(bodyData, nowCount, CHUNKED_SIZE);
                                outputStream.Flush();
                                nowCount += CHUNKED_SIZE;
                            }
                        }

                    }
                    catch (Exception ex)
                    {
                        if (errorLog != null)
                        {
                            errorLog.Write(ex.Message);
                        }
                    }
                    finally
                    {
                        outputStream.Close();
                        outputStream.Dispose();
                    }


                    return true;
                }
                else
                {

                    response.BinaryWrite(bodyData);



                    return true;
                }
            }


            return false;
        }

        bool IsClientProvidedGZip(HttpContext context)
        {
            string client_accept_encoding = context.Request.Headers.Get("Accept-Encoding") ?? string.Empty;
            //支持GZip
            if (client_accept_encoding.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) >= 0)
            {
                return true;
            }
            return false;
        }

        void EchoBrowserCache(RequestInfo info, Store.StoreData data)
        {
            HttpContext context = info.Context;

            if (data != null)
            {
                string cc = info.Rule.ConfigRule.CacheControl ?? string.Empty;

                switch (cc)
                {
                    case "private":
                        context.Response.Cache.SetCacheability(HttpCacheability.Private);
                        break;

                    case "no-store":
                        context.Response.Cache.SetNoStore();
                        break;
                    case "no-cache":
                        context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
                        break;

                    case "public":
                    default:
                        if (true)
                        {
                            string time = data.CreatedDate.ToString("r");
                            TimeSpan ts = TimeSpan.FromSeconds(data.Seconds);
                            DateTime now = DateTime.Now;
                            DateTime expDate = now.Add(ts);

                            context.Response.Cache.SetCacheability(HttpCacheability.Public);
                            context.Response.Cache.SetExpires(expDate);
                            context.Response.Cache.SetMaxAge(ts);//cdn 缓存时间
                            context.Response.Cache.SetLastModified(data.CreatedDate);
                        }
                        break;
                }
            }
        }

        void EchoContentEncodingCharset(HttpContext context, string contentType)
        {
            if (!string.IsNullOrEmpty(contentType))
            {
                string sign = "charset=";
                int charsetIndex = contentType.IndexOf(sign);
                int charsetValueIndex = charsetIndex + sign.Length;
                if (charsetIndex > 0 && charsetValueIndex < contentType.Length)
                {
                    string charset = contentType.Substring(charsetValueIndex);

                    context.Response.ContentEncoding = Encoding.GetEncoding(charset);

                    context.Response.Charset = charset;
                }
            }
        }

        //输出浏览器缓存
        /*
        bool EchoClientCache(HttpContext context, int cacheSeconds)
        {
            bool succ = false;

            try
            {
                string if_modified_since = context.Request.Headers["If-Modified-Since"] ?? string.Empty;

                if (!string.IsNullOrEmpty(if_modified_since))
                {
                    DateTime if_date;
                    if (DateTime.TryParse(if_modified_since, out if_date))
                    {
                        if (if_date.AddSeconds(cacheSeconds) > DateTime.Now)
                        {
                            context.Response.StatusCode = (int)System.Net.HttpStatusCode.NotModified;
                            context.Response.StatusDescription = "from PageCache";

                            succ = true;
                        }
                    }
                }
            }
            catch
            {

            }

            return succ;
        }
        */


        bool BeFilterHeader(string headerKey)
        {
            string headersFilters = this.setting.Config.HeadersFilters;

            if (string.IsNullOrEmpty(headersFilters))
            {
                return false;
            }

            if (headersFilters.IndexOf(',') > 0)
            {
                string[] prefixs = headersFilters.Split(',');
                for (int i = 0; i < prefixs.Length; i++)
                {
                    string prefix = prefixs[i];

                    if (headerKey.IndexOf(headersFilters) >= 0)
                    {
                        return true;
                    }
                }
            }
            else
            {
                return headerKey.IndexOf(headersFilters) >= 0;
            }

            return false;
        }




        //int GetStatusCacheSeconds(int statusCode)
        //{
        //    int returnCacheSecond = -1;

        //    int outStatusCode = 0;
        //    for (int i = 0; i < this.setting.Config.Expires.Count; i++)
        //    {
        //        var expire = this.setting.Config.Expires[i];

        //        if (!string.IsNullOrEmpty(expire.Code))
        //        {
        //            if (expire.Code.IndexOf("-") > 0)
        //            {
        //                int rangeSignIndex = expire.Code.IndexOf('-');

        //                int maxIndex = rangeSignIndex + 1;

        //                if (rangeSignIndex > 0 && maxIndex < expire.Code.Length)
        //                {

        //                    string minString = expire.Code.Substring(0, rangeSignIndex);

        //                    string maxString = expire.Code.Substring(maxIndex);

        //                    int min, max;

        //                    if (int.TryParse(minString, out min) && int.TryParse(maxString, out max))
        //                    {

        //                        if (min <= statusCode && statusCode <= max)
        //                        {
        //                            returnCacheSecond = expire.Seconds;
        //                            break;
        //                        }
        //                    }
        //                }

        //            }
        //            else if (int.TryParse(expire.Code, out outStatusCode))
        //            {
        //                if (outStatusCode == statusCode)
        //                {
        //                    returnCacheSecond = expire.Seconds;
        //                    break;
        //                }


        //            }
        //            else if (expire.Code == "*")
        //            {
        //                returnCacheSecond = expire.Seconds;
        //            }
        //        }
        //    }

        //    return returnCacheSecond;
        //}

        string GetRequestHeadersData(RequestInfo info)
        {
            Uri uri = new Uri(info.UriString);

            var rsb = new StringBuilder();

            rsb.AppendFormat("{0} {1} HTTP/1.1\r\n", info.HttpMethod, uri.PathAndQuery);

            if (info.HostAddress.Port == 80)
            {
                rsb.AppendFormat("Host: {0}\r\n", uri.Host);
            }
            else
            {
                rsb.AppendFormat("Host: {0}:{1}\r\n", uri.Host, info.HostAddress.Port);
            }

            var formsb = new StringBuilder();

            if (info.Form.Count > 0)
            {
                bool first = true;

                foreach (string key in info.Form.Keys)
                {
                    if (!first)
                    {
                        formsb.Append("&");
                    }

                    string value = HttpUtility.UrlEncode(info.Form[key] ?? string.Empty);

                    formsb.AppendFormat("{0}={1}", key, value);

                    first = false;
                }
            }


            //http 1.1 中所有的连接都是 keep-alive
            //requestStringBuilder.AppendFormat("Connection: keep-alive\r\n");

            int contentLength = formsb.Length;
            if (contentLength > 0)
            {
                rsb.AppendFormat("Content-Type: application/x-www-form-urlencoded\r\n");
                rsb.AppendFormat("Content-Length: {0}\r\n", contentLength);
            }

            rsb.AppendFormat("Connection: close\r\n");

            foreach (string k in info.Headers.Keys)
            {
                if (!string.IsNullOrEmpty(k))
                {
                    rsb.AppendFormat("{0}: {1}\r\n", k, info.Headers[k]);
                }
            }

            rsb.Append("\r\n");


            if (contentLength > 0)
            {
                rsb.Append(formsb);

                rsb.Append("\r\n");
            }


            return rsb.ToString();
        }

        bool IsErrorHttpStatusCode(int httpStatusCode)
        {

            string scode = httpStatusCode.ToString();
            char scodep = '0';

            if (!string.IsNullOrEmpty(scode))
            {
                if (scode.Length > 0)
                {
                    scodep = scode[0];
                }
            }

            switch (scodep)
            {

                case '1':
                case '2':
                case '3':
                    return false;


                case '4':
                case '5':
                case '6':
                    return true;


                default:
                    return true;
            }


        }


        Store.StoreData ConvertHttpDataToStoreData(RequestInfo info, Common.HttpData httpdata)
        {

            Store.StoreData data = new Store.StoreData();
            data.CreatedDate = DateTime.Now;
            data.Key = info.Key;
            data.Type = info.Type;
            data.ExpiresAbsolute = DateTime.Now;

            string data_accept_encoding = httpdata.Headers["Content-Encoding"] ?? string.Empty;

            bool echoGZip = IsClientProvidedGZip(info.Context);

            if (httpdata.BodyData != null && httpdata.ContentLength > 0)
            {
                if (data_accept_encoding.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) >= 0)
                {
                    if (echoGZip)
                    {
                        data.BodyData = httpdata.BodyData;
                    }
                    else
                    {
                        data.BodyData = Common.GZip.GZipDecompress(httpdata.BodyData);
                    }
                }
                else
                {
                    if (echoGZip)
                    {
                        data.BodyData = Common.GZip.GZipCompress(httpdata.BodyData);
                    }
                    else
                    {
                        data.BodyData = httpdata.BodyData;
                    }
                }
            }
            else
            {
                data.BodyData = new byte[0];
            }




            if (httpdata.Headers.AllKeys.Contains("Server"))
            {
                httpdata.Headers.Remove("Server");
            }

            if (httpdata.Headers.AllKeys.Contains("Connection"))
            {
                httpdata.Headers.Remove("Connection");
            }

            if (httpdata.Headers.AllKeys.Contains("Content-Length"))
            {
                httpdata.Headers.Remove("Content-Length");
            }

            if (httpdata.Headers.AllKeys.Contains("Date"))
            {
                httpdata.Headers.Remove("Date");
            }

            if (httpdata.Headers.AllKeys.Contains("Transfer-Encoding"))
            {
                httpdata.Headers.Remove("Transfer-Encoding");
            }

            if (httpdata.Headers.AllKeys.Contains("Cache-Control"))
            {
                httpdata.Headers.Remove("Cache-Control");

                //if (httpdata.Headers["Cache-Control"] == "private")
                //{
                //    httpdata.Headers.Remove("Cache-Control");
                //}
            }


            if (echoGZip)
            {
                httpdata.Headers["Content-Encoding"] = "gzip";
            }

            StringBuilder headers = new StringBuilder();
            headers.Append(httpdata.Scheme);
            headers.Append(" ");
            headers.Append(httpdata.StatusCode.ToString());
            headers.Append(" ");
            headers.Append(httpdata.StatusDescription);
            headers.Append("\r\n");

            foreach (string k in httpdata.Headers.Keys)
            {

                if (BeFilterHeader(k))
                {
                    continue;
                }

                string v = httpdata.Headers[k];
                headers.Append(k);
                headers.Append(": ");
                headers.Append(v);
                headers.Append("\r\n");
            }

            byte[] headersData = Encoding.ASCII.GetBytes(headers.ToString());



            //int statusCacheSeconds = GetStatusCacheSeconds(httpdata.StatusCode);

            int statusCacheSeconds = 0;

            data.HeadersData = headersData;

            //错误HTTP Code
            if (IsErrorHttpStatusCode(httpdata.StatusCode))
            {
                statusCacheSeconds = this.config.ErrorCacheSeconds;
            }
            else
            {
                statusCacheSeconds = info.Rule.ConfigRule.CacheSeconds;
            }


            if (statusCacheSeconds > 0)
            {
                data.ExpiresAbsolute = data.CreatedDate.AddSeconds(statusCacheSeconds);
                data.Seconds = statusCacheSeconds;
            }
            else
            {
                data.ExpiresAbsolute = data.CreatedDate;
                data.Seconds = 0;
            }


            return data;
        }




    }

}
